package org.pyant.tasks;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.resources.FileResource;

/**
 * Runs <a href=http://nedbatchelder.com/code/modules/coverage.html>coverage.py</a>
 * and outputs a <a
 * href=http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/491274>colorized</a>
 * visualisation of coverage. <p/> Sample usage: <p/> <py-cover
 * pythonpath="${user.dir}:${user.dir}/client" > <fileset dir="testscripts2">
 * <include name="** /*Test.py"/> </fileset> </py-cover> <p/> Where:
 * <ol>
 * <li> <code>pythonpath</code> is PYTHONPATH, of course (optional)
 * <li> <code>&lt;tests&gt;</code> contains tests to run.
 * </ol>
 * 
 * 
 * @see http://nedbatchelder.com/code/modules/coverage.html
 * 
 * @see http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/491274
 * 
 * @author "<a href=mailto:grisha@alum.mit.edu>Gregory Golberg</a>"
 */
public class PythonCoverageTask extends // PythonBaseTask {
		PythonTestTask {

	private class CoverageData {

		String file;

		String statements;

		String exec;

		String cover;
		
		public String toString() {
			return "<Coverage for " + this.file + ": " + this.statements + ", "
					+ this.exec + "," + this.cover + ">";
		}
	}
	
	private class StdoutCollector extends Thread {

		private boolean done = false;

		public synchronized boolean isDone() {
			return this.done;
		}
		
		private boolean error = false;

		private BufferedReader stdout;

		private List coverageDataList = new ArrayList();
		
		private StdoutCollector(Process p) {
			this.stdout = new BufferedReader(new InputStreamReader(p
					.getInputStream()));
		}


		public void run() {
			String line = null;
			try {
				int lineCount = 0;
				while ((line = this.stdout.readLine()) != null) {
					if (lineCount >= 2) {
						if (line.startsWith("------")) {
							break;
						}
						String[] result = line.split("[ \t]+");
						log("Result: " + Arrays.asList(result).toString(),
								Project.MSG_DEBUG);
						CoverageData cd = new CoverageData();
						cd.file = result[0];
						cd.statements = result[1];
						cd.exec = result[2];
						cd.cover = result[3];
						this.coverageDataList.add(cd);
					}
					lineCount++;
				}

			} catch (IOException ioe) {
				synchronized (this) {
					log("Error: " + ioe);
					this.error = true;
				}
			}
			synchronized (this) {
				this.done = true;
			}
		}
	}

	/**
	 * ` Location of coverage.py
	 */
	protected File coverage;

	/**
	 * @see #coverage
	 */
	public void setCoverage(File coverage) {
		this.coverage = coverage;
	}

	/**
	 * Test suites (.py files) to run
	 */
	private FileSet tests;

	/**
	 * @see #tests
	 */
	public void addTests(FileSet tests) {
		if (this.tests != null) {
			throw new BuildException("Only one FileSet allowed in <tests>");
		}
		this.tests = tests;
	}

	private FileSet src;

	public void addSrc(FileSet src) {
		if (this.src != null) {
			throw new BuildException("Only one FileSet allowed in <src>");
		}
		this.src = src;
	}

	protected File reportsdir;

	public void setReportsdir(File reportsDir) {
		this.reportsdir = reportsDir;
	}

	private String[] pythonCmd = null;

	private File testDriver;

	private void setup() {
		if (this.coverage == null) {
			throw new BuildException("Coverage not defined");

			// BufferedReader reader = new BufferedReader(new InputStreamReader(
			// getClass().getClassLoader().getSystemResourceAsStream(
			// "org/pyant/tasks/coverage.py")));
			//
			// try {
			// this.coverage = new File(getProject().getBaseDir(),
			// "coverage.py");
			// PrintWriter writer = new PrintWriter(new FileWriter(
			// this.coverage));
			//
			// String line = "";
			// while ((line = reader.readLine()) != null) {
			// writer.println(line);
			// }
			// reader.close();
			// writer.close();
			// } catch (IOException ioe) {
			// throw new BuildException(
			// "coverage attribute not specified and cannot create it",
			// ioe);
			// }
		}

		pythonCmd = new String[] { getPython(), "-u", "-O",
		// "-v",
				this.coverage.getAbsolutePath() };

		this.env = new String[] { "PYTHONPATH=" + this.pythonpath.toString() };
		log("PYTHONPATH=" + this.pythonpath, Project.MSG_VERBOSE);
		try {
			this.testDriver = File.createTempFile("testDriver", ".py");
			PrintWriter writer = new PrintWriter(
					new FileWriter(this.testDriver));
			writer.println("import unittest");
			writer.println("import sys");
			writer.println("if __name__  == '__main__':");
			writer.println("\targv = sys.argv[1:]");
			writer
					.println("\tprint \"testDriver called with \" + ''.join(argv)");
			writer
					.println("\tunittest.main(module=__import__(sys.argv[1]), defaultTest=None, argv=[sys.argv[0]])");
			// , defaultTest=None, argv=[sys.argv[1]])");
			writer.close();
		} catch (IOException ioe) {
			throw new BuildException(ioe);
		}
		log("Test driver: " + this.testDriver);

	}

	private String[] env;

	private void report(String test) throws IOException, InterruptedException {
		log("Creating report for: " + test);
		File dir = this.reportsdir;
		if (test != null) {
			dir = new File(dir, new File(test).getName());
			if (!dir.mkdir()) {
				throw new BuildException("Cannot create " + dir);
			}
		}

		String title = test == null ? getProject().getName() : test;

		PrintWriter writer = new PrintWriter(new FileWriter(new File(dir,
				test == null ? "index2.html" : "index.html")));

		writer.println("<html><head><title>" + title + "</title></head>");
		writer.println("<body><h1>" + title + "</h1>");
		writer.println("<table border=1>");
		writer
				.println("<tr><th>File</th><th>Statements</th><th>Exec</th><th>Cover %</th></tr>");

		List coverageData = new ArrayList();
		
		List curList = new ArrayList();
		for (int i = 0; i < this.srcList.size(); i += 10) {
			curList.clear();
			curList.add("-r");
			for (int j = i; j < (i + 10 > this.srcList.size() ? this.srcList.size()
					: i + 10); j++) {
				FileResource res = (FileResource) srcList.get(j);
				File file = new File(res.getBaseDir(), res.getName());
				curList.add(file.getAbsolutePath());
			}
			
			Process p = executeCoverage((String[])curList.toArray(new String[]{}));
						
			StdoutCollector sc = new StdoutCollector(p);
			sc.start();
			while (!sc.isDone()) {
				try {
					Thread.sleep(10);
				} catch (InterruptedException ie) {

				}
			}
			coverageData.addAll(sc.coverageDataList);
			p.waitFor();
				
			curList.set(0, "-d");
			curList.add(1, dir.getAbsolutePath());
			curList.add(2, "-a");
			
			simpleExecuteCoverage((String[])curList.toArray(new String[]{}));

			for (int k = 3; k < curList.size();k++) {
				File file = new File(curList.get(k).toString());
				File cover = new File(dir, file.getName() + ",cover");
				File coverHtml = new File(dir, file.getName() + ".html");
				PrintWriter htmlWriter = new PrintWriter(new FileWriter(
						coverHtml));

				htmlWriter.println("<html><head/><body>");

				BufferedReader reader = new BufferedReader(
						new FileReader(cover));
				String pythonLine = "";
				int lineCount = 1;
				while ((pythonLine = reader.readLine()) != null) {
					String color = "#FFFFFF";
					if (pythonLine.startsWith(">")
							|| pythonLine.startsWith("!")) {
						color = pythonLine.charAt(0) == '>' ? "#00FF00"
								: "#FF0000";
						pythonLine = pythonLine.substring(1);
					}
					pythonLine = pythonLine.replaceAll(" ", "&nbsp;");
					htmlWriter
							.println("<span style='font-style: \"Courier New\"; background-color: "
									+ color
									+ "'>"
									+ pythonLine
									+ "</span><br/>");
				}
				htmlWriter.println("</body></html>");
				htmlWriter.close();
				reader.close();
			}
		
		}

		Collections.sort(coverageData, new Comparator() {
			public int compare(Object o1, Object o2) {
				CoverageData sc1 = (CoverageData) o1;
				CoverageData sc2 = (CoverageData) o2;
//				log("Comparing: " + sc1 + " vs " + sc2, Project.MSG_VERBOSE);
				int percentage1 = Integer.parseInt(sc1.cover.substring(0,
						sc1.cover.length() - 1));
				int percentage2 = Integer.parseInt(sc2.cover.substring(0,
						sc2.cover.length() - 1));

				return percentage1 == percentage2 ? 0
						: (percentage1 > percentage2 ? -1 : 1);
			}

		});

		for (Iterator iter = coverageData.iterator(); iter.hasNext();) {
			CoverageData sc = (CoverageData) iter.next();
			log(sc.file + " " + sc.statements + " " + sc.exec + " " + sc.cover);
			String html = new File(sc.file).getName() + ".py.html";
			writer.print("<tr><td><a href=" + html + ">" + sc.file
					+ "</a></td>");
			writer.print("<td>" + sc.statements + "</td>");
			writer.print("<td>" + sc.exec + "</td>");
			writer.print("<td>" + sc.cover + "</td>");
			writer.println("</tr>");
			writer.flush();
		}

		writer.println("</table>");
		writer.println("</body></html>");
		writer.close();

	}

	/**
	 * Runs the coverage (executes the specified Python file)
	 * 
	 * @param test
	 *            file to execute
	 * 
	 * @throws IOException
	 * 
	 * @throws InterruptedException
	 */
	private void runCoverage(String test) throws IOException,
			InterruptedException {
		// minus .py
		test = test.substring(0, test.length() - 3);
		if (!this.packagedtests) {
			test = test.substring(test.lastIndexOf(File.separatorChar) + 1);
		}
		test = test.replaceAll("\\\\", ".");
		test = test.replaceAll("/", ".");
		log("Running coverage on " + test);
		simpleExecuteCoverage(new String[] { "-x",
				this.testDriver.getAbsolutePath(), test });
	}

	/**
	 * Runs "python coverage.py" command with specified arguments, waiting until
	 * the exec()'ed Process exits.
	 * 
	 * @param args
	 */
	private void simpleExecuteCoverage(String[] args) {
		final Process p = executeCoverage(args);

		try {
			final BufferedReader err = new BufferedReader(
					new InputStreamReader(p.getErrorStream()));
			final BufferedReader out = new BufferedReader(
					new InputStreamReader(p.getInputStream()));

			new Thread() {
				public void run() {
					String line = "";
					try {
						while ((line = err.readLine()) != null) {
							PythonCoverageTask.this.log("stderr> " + line,
									Project.MSG_VERBOSE);
						}
					} catch (Exception e) {
					}
					PythonCoverageTask.this.log("stderr Done",
							Project.MSG_VERBOSE);
				}
			}.start();

			new Thread() {
				public void run() {
					String line = "";
					try {
						while ((line = out.readLine()) != null) {
							PythonCoverageTask.this.log("stdout> " + line,
									Project.MSG_VERBOSE);
						}
					} catch (Exception e) {
					}
					PythonCoverageTask.this.log("stdout Done",
							Project.MSG_VERBOSE);
				}
			}.start();
			// log("");
			System.out.flush();
			System.err.flush();
			int exitCode = p.waitFor();
			// if (exitCode != 0) {
			// throw new BuildException(this.coverage
			// + " "
			// + Arrays.asList(args).toString().replaceAll(",", " ")
			// .replace("[", "").replace("]", "")
			// + " failed: " + exitCode);
			// }
		} catch (Exception e) {
			throw new BuildException(e);
		}
	}

	private void eraseCoverage() {
		log("Erasing coverage");
		simpleExecuteCoverage(new String[] { "-e" });
	}

	/**
	 * Runs python coverage.py command with specified arguments.
	 * 
	 * @param args
	 *            arguments to the coverage.py
	 * 
	 * @return Process representing the forked Python process
	 */
	private Process executeCoverage(String[] args) {
		String newArgs[] = new String[this.pythonCmd.length + args.length];

		for (int i = 0; i < this.pythonCmd.length; i++) {
			newArgs[i] = this.pythonCmd[i];
		}

		for (int i = 0; i < args.length; i++) {
			newArgs[this.pythonCmd.length + i] = args[i];
		}

		File curDir = new File(System.getProperty("user.dir"));
		log("Executing in "
				+ curDir
				+ ":\n\t"
				+ Arrays.asList(newArgs).toString().replaceAll("\\[", "")
						.replaceAll("\\]", "").replaceAll(",", " "),
				Project.MSG_VERBOSE);

		IOException exc = null;
		for (int i = 0; i < 5; i++) {
			try {
				Process p = Runtime.getRuntime()
						.exec(newArgs, this.env, curDir);
				return p;
			} catch (IOException e) {
				log("Problem running coverage", e, Project.MSG_INFO);
				log("Will try again...");
				try {
					Thread.sleep(2000);
				} catch (InterruptedException ie) {

				}
				exc = e;
			}
		}
		throw new BuildException(exc);
	}

	private List srcList = new ArrayList(); 
	
	/**
	 * Main entry point.
	 * 
	 * @see Task#execute
	 */
	public void execute() {
		try {
			setup();

			this.srcList = new ArrayList();
			for (Iterator iter = this.src.iterator();iter.hasNext();) {
				srcList.add(iter.next());
			}
			
			List testNames = new ArrayList();
			log("Doing test-by-test reports");
			for (Iterator iter = tests.iterator(); iter.hasNext();) {
				FileResource res = (FileResource) iter.next();
				String test = new File(res.getBaseDir(), res.getName())
						.getAbsolutePath();
				eraseCoverage();
				log(new Date() + " Executing test: " + test);
				testNames.add(new File(test).getName());
				runCoverage(test);
				report(test);
			}

			log("Doing cumulative report");
			eraseCoverage();
			for (Iterator iter = tests.iterator(); iter.hasNext();) {
				FileResource res = (FileResource) iter.next();
				String test = new File(res.getBaseDir(), res.getName())
						.getAbsolutePath();
				log("Executing test: " + test);
				runCoverage(test);
			}
			report(null);

			PrintWriter finalWriter = new PrintWriter(new FileWriter(new File(
					this.reportsdir, "index.html")));

			finalWriter
					.println("<html><head><title>Coverage report</title></head><body>");
			finalWriter.println("<h3>Reports</h3>");
			finalWriter.println("<ul>");
			finalWriter
					.println("<h3><a href=index2.html>Cumulative report</a></h3>");
			finalWriter.println("<hr/>");
			finalWriter.println("<h3>Report by test</h3>");
			finalWriter.println("<ul>");

			for (Iterator iter = testNames.iterator(); iter.hasNext();) {
				String name = (String) iter.next();
				finalWriter.println("<li><A href=" + name + "/index.html>"
						+ name + "</li>");
			}

			finalWriter.println("</ul>");
			finalWriter.println("</body></html>");
			finalWriter.close();

		} catch (Exception e) {
			throw new BuildException(e);
		}
	}
}